#!/usr/bin/env node

/*
example:
#!/usr/bin/env xkettle

@json('1.json')
->uniq()
->json('2.json')
*/
const _ = require('lodash');
const moment = require('moment');
const fs = require('fs-extra');
const utils = require('./js/utils');
const kettle = require('./js/kettle');
const { Writable } = require("stream");
let debug = 0; // 开启调试模式

function showHelp(item, more) {
  if (!item) {
    console.log('Usage:');
    console.log('    xkettle scriptFile [-v] [params]');
    console.log('    xkettle -s script [-v] [params]');
    console.log('    xkettle -h|--help [server|reader|writer|transform] [more]');
    console.log('');
    console.log('command:');
    console.log(`    xkettle -s "@list([1,1,2,2])->uniq()->console()"`);
    console.log('');
    console.log('script:');
    console.log('    #!/usr/bin/env xkettle');
    console.log(`    @list([1,1,2,2])->uniq()->console()`);
    console.log('');
    console.log('reader:');
    console.log('    @list, @file, @clip, @json, @excel, @dir, @mongodb, @mysql, @oracle, @sqlserver');
    console.log('writer:');
    console.log('    console, file, text, json, js, excel, md, mongodb, mysql, oracle, sqlserver');
    console.log('transform:');
    console.log('    log, field, flat, map, format, sort, uniq, array, slice, split, filter, group, wait, get, post, script, html, dhtml, article, imageSave');
    console.log('分流：');
    console.log('    @list([1,1,2,2])->[temp]uniq()->console();@temp->log()');
    console.log('服务模式:');
    console.log('    只有开启服务器模式的时候使用，可以使用@get和@post');
    console.log(`    @get('/api', option):@list([1,2])`);
    console.log('参数:');
    console.log('    全局参数存入$params中，使用：@list($params)');
  } else if (item === '@get') {
    console.log('启服务器模式get接口：');
    console.log(`@get('/api', paramSet, { p=5189, t: 0 } = {}):reader`);
    console.log('    :开启接口为/api的get服务，返回数据类型为json')
    console.log('    paramSet: 设置参数，如：(o, utils) => o, 其中o为query，在后面的所有的流都可以使用这些变量，可使用$.page的变量，如果为空，则$为query，若第二项不是函数，则为option')
    console.log(`    如:@get('/api', o=>({page:o.page}), { p:5189 }):@list([+$.page,+$.page+1])`)
    console.log(`       @get('/api', { p:5189 }):@list([+$.page,+$.page+1])`)
    console.log('    option: p: 端口, t: 延时时间(秒)')
  } else if (item === '@post') {
    console.log('启服务器模式post接口：');
    console.log(`@post('/api', paramSet, { p=5189, t: 0 } = {}):reader`);
    console.log('    :开启接口为/api的post服务，返回数据类型为json')
    console.log('    paramSet: 设置参数，如：(o, utils) => o, 其中o为body，在后面的所有的流都可以使用这些变量，可使用$.page的变量，如果为空，则$为body，若第二项不是函数，则为option')
    console.log(`    如:@post('/api', o=>({page:o.page}), { p:5189 }):@list([+$.page,+$.page+1])`)
    console.log(`       @post('/api', { p:5189 }):@list([+$.page,+$.page+1])`)
    console.log('    option: p: 端口, t: 延时时间(秒)')
  } else if (item === '@list') {
    console.log('将数组转化为流：');
    console.log('@list(list)');
    console.log('    :参数为数组, 将数组转化为流')
  } else if (item === '@clip') {
    console.log('将剪切板的内容转化为流：');
    console.log(`@clip(options) // 获取剪切板的内容`)
    console.log(`    一、 options为true|1或者function使用text`)
    console.log(`    1. function: 修正剪切板的内容为流入参(text)，遵循 parseFunction`)
    console.log(`    二、如果options为空|字符串[sep]或者 { sep, keepEmpty, inFormat, format }，则会对剪切板的内容进行解析或者切割`)
    console.log(`    1. 如果剪切板中的能解析为数组，则使用解析的数组为初始值`)
    console.log(`    2. 如果设置了 sep，则用sep分割为数组`)
    console.log(`    3. 否则如果剪切板的字符串是多行，则用 /\\r?\\n/ 分割`)
    console.log(`    4. 否则用 /\\s+|，|,|；|;/ 分割为数组，空格|逗号|分号|中文逗号|中文分号`)
    console.log(`    5. 如果keepEmpty为true，则获保留空白的行，默认是过滤掉空白的行`)
    console.log(`    6. 可以使用inFormat修改item，遵循 parseFunction`)
    console.log(`    7. 可以使用format修改list，遵循 parseFunction`)
    console.log(`例子：`)
    console.log(`    @clip() // 默认分割`)
    console.log(`    @clip(1|true) // 使用text`)
    console.log(`    @clip(text=>...) // 用函数修正text`)
    console.log(`    @clip(',') // 使用,分割`)
    console.log(`    @clip({ sep, keepEmpty, inFormat, format }) // 分割并修正入参`)
  } else if (item === '@excel') {
    console.log('将excel的内容转化为流：');
    console.log(`@excel(file, { sheet = 0, startRow = 1, startCol = 1, hasTitle: true })`)
    console.log(`    sheet:第几个sheet, 从0开始`)
    console.log(`    startRow: 第几行, 从1开始`)
    console.log(`    startCol: 第几列, 从1开始`)
    console.log(`    hasTitle: 是否有标题`)
  } else if (item === '@json') {
    console.log('将json文件的内容转化为流：');
    console.log(`@json(file, type)`)
    console.log(`    type默认为空, 可取值: module, function`)
    console.log(`    默认: 内容如:  [{1, _.random(1, 10), 2}]`)
    console.log(`    module: 内容如: module.exports = [1,2,3]`)
    console.log(`    function: 内容如: ({ _, moment, utils })=>[{1, _.random(1, 10), 2}]`)
  } else if (item === '@dir') {
    console.log('扫描文件夹转化为流：');
    console.log(`@dir(dir, { type, format })`)
    console.log(`    如果 type === 1， 显示文件夹列表`)
    console.log(`    如果 type === 2， 显示文件列表`)
    console.log(`    默认type为空， 显示所有列表`)
    console.log(`    可以使用format修改list`)
    console.log(`    list 格式为:  [{isdir: true, name: 'xx', fullname: 'yy/xx'}, { name: 'xx.js', purename: 'xx', extname: '.js', fullname: 'yy/xx.js'}]`)
  } else if (item === '@file') {
    console.log('将文件内容转化为流：');
    console.log(`@file(file, { isText, sep, format, keepEmpt, encoding })`)
    console.log(`    1. encoding 默认为 utf-8 （其他选项与clip一致）`)
    console.log(`一、 options为true|1或者options.isText为true或者function使用text`)
    console.log(`    1. function: 修正剪切板的内容为流入参(text)，遵循 parseFunction`)
    console.log(`二、如果options为空|字符串[sep]或者 { sep, keepEmpty, inFormat, format }，则会对剪切板的内容进行解析或者切割`)
    console.log(`    1. 如果剪切板中的能解析为数组，则使用解析的数组为初始值`)
    console.log(`    2. 如果设置了 sep，则用sep分割为数组`)
    console.log(`    3. 否则如果剪切板的字符串是多行，则用 /\\r?\\n/ 分割`)
    console.log(`    4. 否则用 /\\s+|，|,|；|;/ 分割为数组，空格|逗号|分号|中文逗号|中文分号`)
    console.log(`    5. 如果keepEmpty为true，则获保留空白的行，默认是过滤掉空白的行`)
    console.log(`    6. 可以使用inFormat修改item，遵循 parseFunction`)
    console.log(`    7. 可以使用format修改list，遵循 parseFunction`)
    console.log(`例子：`)
    console.log(`    @file(file) // 默认分割`)
    console.log(`    @file(file, 1|true) // 使用text`)
    console.log(`    @file(file, text=>...) // 用函数修正text`)
    console.log(`    @file(file, ',') // 使用,分割`)
    console.log(`    @file(file, { isText: 1 }) // 使用text`)
    console.log(`    @file(file, { sep, keepEmpty, inFormat, format }) // 分割并修正入参`)
  } else if (item === '@mongodb') {
    console.log('读取mongodb转化为流：');
    console.log(`@mongodb({ uri = 'mongodb://localhost:27017', database, table, cond, sort, count=1 })`)
    console.log(`    通过cond条件按照sort排序查找database中table的数据列表`)
    console.log(`    默认数量为1`)
    console.log(`    例: xkettle -s "@mongodb({database: 'test', table: 'member'})->log()"`)
  } else if (item === '@mysql') {
    console.log('读取mysql转化为流：');
    console.log(`@mysql({ host = 'localhost', port = 3306, user = 'root', password, database, table, sql, count=1 })`)
    console.log(`    通过sql查找database中table的数据列表`)
    console.log(`    默认sql为: select * from database limit count`)
    console.log(`    例: xkettle -s "@mysql({database: 'test', table: 'member'})->log()"`)
  } else if (item === '@oracle') {
    console.log('读取oracle转化为流：');
    console.log(`@oracle({ user, password, connectString, table, sql, count = 1 })`)
    console.log(`    通过sql查找database中table的数据列表`)
    console.log(`    默认sql为: select * from database where rownum<= count`)
  } else if (item === '@sqlserver') {
    console.log('读取sqlserver转化为流：');
    console.log(`@sqlserver({ user, password, server, database, table, sql, count = 1 })`)
    console.log(`    通过sql查找database中table的数据列表`)
    console.log(`    默认sql为: select top count * from database`)
  } else if (item === 'log') {
    console.log('打印流日志：');
    console.log(`->log({ inFormat })`)
    console.log(`    inFormat: chunk格式化，遵循 parseFunction`)
  } else if (item === 'field') {
    console.log('字段转化：');
    console.log(`->field(list, hasOldName = false)`)
    console.log(`list: [`)
    console.log(`        ['a->b', o => ...], // 将旧的字段 a 修改为新字段 b， 值得变换为函数转化，其中参数为 value[a]`)
    console.log(`        'a', // 保留原来得 a 字段`)
    console.log(`        { name: 'a', newName: 'b', value: o => ... }, // 将旧的字段 a 修改为新字段 b， 值得变换为函数转化，其中参数为 value[a]`)
    console.log(`        { name: 'a', value: '$+50+_.random(1, 10)' }, // 参见 parseFunction`)
    console.log(`        { name: 'a', newName: 'b' }, // 将旧的字段 a 修改为新字段 b， 值不变`)
    console.log(`        { name: '$', newName: 'b' }, // 使用新得字段 b 代替原来的根值`)
    console.log(`        { name: 'a', newName: '$' }, // 使用旧的字段 a 作为根值`)
    console.log(`        { name: 'm', value: 5 }, // 设置新字段m，值为5`)
    console.log(`        { name: 'm', value: '=>$$.a' }, // 设置新字段m，值为原来值的a字段`)
    console.log(`    ]`)
    console.log(`    hasOldName: true|[false], // 是否包含旧的的字段，如果包含，则用chunk，否则用{}为初始化对象`)
  } else if (item === 'flat') {
    console.log(`将数组类型thunk的每一项推送到流中：`)
    console.log(`->flat()`)
  } else if (item === 'format') {
    console.log('格式化：');
    console.log(`->format(type, inFormat)`)
    console.log(`    type: 格式:  0: js 1: json 2:inline-js 3:inline-json`)
    console.log(`    inFormat: 修正流入参，遵循 parseFunction`)
    console.log(``)
    console.log(`例子：`)
    console.log(`    ->format() // 不做任何转换`)
    console.log(`    ->format(0) // 转化为js模式`)
    console.log(`    ->format(o=>JSON.stringify(o)) // 转化为JSON字符串`)
    console.log(`    ->format(0, o=>\`[\${o}]\`) // 先转化为js模式，在添加中括号`)
  } else if (item === 'map') {
    console.log('映射：');
    console.log(`->map(field|function)`)
    console.log(`    同lodash`)
  } else if (item === 'object') {
    console.log('纯Object格式化：');
    console.log(`->object()`)
    console.log(`    转化为纯Object格式，原理: JSON.parse(JSON.stringify(chuck))`)
  } else if (item === 'wait') {
    console.log(`等待：`)
    console.log(`->wait(ms)`)
    console.log(`    ms: 等待毫秒数`)
  } else if (item === 'get') {
    console.log(`http get请求：`)
    console.log(`->get({ url, inFormat, outFormat })`)
    console.log(`    url: 请求的url`)
    console.log(`    inFormat: chunk格式化，遵循 parseFunction`)
    console.log(`    outFormat: get结果输出格式化，遵循 parseFunction`)
  } else if (item === 'post') {
    console.log(`http post请求：`)
    console.log(`->post({ url, inFormat, outFormat })`)
    console.log(`    url: 请求的url`)
    console.log(`    inFormat: chunk格式化，遵循 parseFunction`)
    console.log(`    outFormat: get结果输出格式化，遵循 parseFunction`)
  } else if (item === 'sort') {
    console.log(`流排序：`)
    console.log(`->sort([iteratees=[_.identity]], [orders])`)
    console.log(`    参考: lodash 的 sortBy`)
    console.log(`    允许指定 iteratee（迭代函数）结果如何排序。 如果没指定 orders（排序），所有值以升序排序。 否则，指定为"desc" 降序，或者指定为 "asc" 升序，排序对应值。`)
    console.log(`    [iteratees=[_.identity]] (Array[]|Function[]|Object[]|string[]): 排序的迭代函数`)
    console.log(`    [orders] (string[]): iteratees迭代函数的排序顺序。`)
    console.log(`例子：`)
    console.log(`    sort(['a', 'b'], ['asc', 'desc']); // 先按照 a 升序，再按照 b 降序`)
    console.log(`    sort(['a', 'b'], [1, -1]); // 先按照 a 升序，再按照 b 降序`)
    console.log(`    sort(); // 先按照本身升序`)
    console.log(`    sort('desc'); // 先按照本身降序`)
    console.log(`    sort('a'); // 先按照 a 字段升序`)
    console.log(`    sort('a', 'desc'); // 先按照 a 字段降序`)
    console.log(`    sort('a.b', 'desc'); // 先按照 a.b 字段降序`)
    console.log(`    sort(o=>o.b, 'desc'); // 先按照 o=>o.b的返回值降序`)
  } else if (item === 'uniq') {
    console.log(`去重：`)
    console.log(`->uniq(func)`)
    console.log(`    如果func为空，则按照JSON.stringify(sortObject(chuck)))来去重`)
    console.log(`    否则按照func的返回值obj传入JSON.stringify(sortObject(obj)))来去重`)
  } else if (item === 'array') {
    console.log(`合并流：`)
    console.log(`->array()`)
    console.log(`    将stream中的每一个thunk合并到一个数组[chunk, chunk, ...]后再加入到流中`)
  } else if (item === 'slice') {
    console.log(`截取流：`)
    console.log(`->slice(start, count)`)
    console.log(`    从stream中截取从start开始的count个thunk，start不能为负数`)
  } else if (item === 'split') {
    console.log(`分割流：`)
    console.log(`@split(sep, { format, keepEmpty })`)
    console.log(`    切割为数组后再加入到流中`)
    console.log(`    1. 如果设置了 sep，则用sep分割为数组`)
    console.log(`    2. 否则如果剪切板的字符串是多行，则用 /\\r?\\n/ 分割`)
    console.log(`    3. 否则用 /\\s+|，|,|；|;/ 分割为数组，空格|逗号|分号|中文逗号|中文分号`)
    console.log(`    4. 如果keepEmpty为true，则获保留空白的行，默认是过滤掉空白的行`)
    console.log(`    5. 可以使用format修改list`)
    console.log(`    @split({ keepEmpty: true }) // 保留空白的行`)
  } else if (item === 'filter') {
    console.log(`过滤流：`)
    console.log(`->filter(func)`)
    console.log(`    如果func返回的是true则通过，否则会被过滤掉，如果func为空，则不过滤`)
  } else if (item === 'group') {
    console.log(`分组流：`)
    console.log(`->group(groupKey, {`)
    console.log(`    [key]: '1', // 取第一个值`)
    console.log(`    [key]: '-1', // 取最后一个值`)
    console.log(`    [key]: '1+', // 取第一个非空值`)
    console.log(`    [key]: '-1+', // 取最后一个非空值`)
    console.log(`    [key]: '+', // 取和`)
    console.log(`    [key]: '/', // 取平均`)
    console.log(`    [key]: '*', // 取乘积`)
    console.log(`    [key]: '_', // 取方根`)
    console.log(`    [key]: 'm', // 取中位数`)
    console.log(`    [key]: 'l', // 取数组`)
    console.log(`})`)
  } else if (item === 'script') {
    console.log(`按照脚本修改流数据：`)
    console.log(`->script(file)`)
    console.log(`    将file使用parseFunction解析为函数后执行\`const obj = await func(chunk, { _, moment, utils })\`，然后将obj加入到流中`)
    console.log(`parseFunction: `)
    console.log(`    function :直接返回为函数`)
    console.log(`    字符串: `)
    console.log(`    '=>123' :返回 123，可以使用 $, _, moment, utils, $$ 等符号`)
    console.log(`    '=$123' :返回 input123 其中 $ 代表当前参数，直接替换`)
    console.log(`    'return 123' :与 '=>123'相同，返回 123，可以使用 $, _, moment, utils, $$ 等符号`)
    console.log(`    '($, {_, moment, utils, $$})=>123' :返回 123`)
    console.log(`    '@1.js' :返回1.js中exports的函数`)
    console.log(`    其他:  直接返回字符串`)
  } else if (item === 'html') {
    console.log(`爬取html：`)
    console.log(`->html({ encoding = 'utf-8', inFormat, outFormat })`)
    console.log(`    encoding: 默认是 utf-8， 可以是 gb2312|utf-8|GBK，解决网页汉字乱码的问题`)
    console.log(`    inFormat: 修正流入参，遵循 parseFunction`)
    console.log(`    outFormat: 修正流出参，遵循 parseFunction`)
    console.log(`    爬虫规则: `)
    console.log(`    流入参格式: { html, selector } 或者 { url, selector }`)
    console.log(`    如果流入参的selector字段是对象，格式为: { group, key: [el, prop]}，用来爬取数组数据，group为每一项元素(为空时只有一项数据), 每一项key为结果键值，el为dom元素，prop为属性(空时为值)`)
    console.log(`    如: xkettle -s "@list([{ html: '<div class=\\"item\\"><a href=\\"href\\">name</a></div>', selector: { group:'.item', name: ['a'], url: ['a', 'href'] } }])->html()->log()"`)
    console.log(``)
    console.log(`    如果流入参的selector字段是字符串，则使用 temme 规则，如:  .item>p@{&{$}} ， 参考: https://temme.js.org`)
    console.log(`    如: xkettle -s "@list([{ html: '<div class=\\"item\\"><a href=\\"href\\">name</a></div>', selector: '.item@{a{\\$name};a[href=\\$url];}' }])->html()->log()"`)
    console.log(``)
    console.log(`    如果流入参的selector字段是函数，则返回jquery的$,如: $=>$('.text').html()，使用 cheerio 解析`)
    console.log(`    如: xkettle -s "@list([{ html: '<div class=\\"item\\"><a href=\\"href\\">name</a></div>', selector: \\$=>({name:\\$('.item a').html(), href:\\$('.item a').attr('href')}) }])->html()->log()"`)
    console.log(``)
    console.log(`    如果出参是数组，则流出参扩展为多层流，如果不想被扩展，使用 outFormat 封装为 object`)
    console.log(``)
    more && console.log(`#### temme 语法`)
    more && console.log(`    支持单行注释 // ...... 与块状注释 /* ...... */，与 JavaScript 保持一致。`)
    more && console.log(`    [foo=$xxx] 放在 CSS 选择器特性匹配部分，用于捕获该特性的值`)
    more && console.log(`    {$xxx} 放在 CSS 选择器之后的花括号内，用于捕获元素的文本内容`)
    more && console.log(``)
    more && console.log(`    <a href="xx">out<span>in</span></a>`)
    more && console.log(`    a[href=$href]{$txt} -> { href: 'xx', txt: 'outin' }`)
    more && console.log(`    a{ $text }; -> {text: 'outin'} // 获取包含的文本`)
    more && console.log(`    a{ $hasAnchor = true }; -> { hasAnchor: true } // 常量赋值`)
    more && console.log(`    a{ html($html) }; -> { html: 'out<span>in</span>' } // 获取html`)
    more && console.log(`    a{ find('out', $website) };; -> { website: 'in' }// 查找`)
    more && console.log(``)
    more && console.log(`    <ul><li data-fruit-id="1"><span data-color="red">apple</span></li></ul>`)
    more && console.log(`数组`)
    more && console.log(`    li@fruits{span[data-color=$color]{$name}&[data-fruit-id=$id];} -> {fruits: [{ color: 'red', name: apple, id: '1' }]}`)
    more && console.log(`分割属性`)
    more && console.log(``)
    more && console.log(`    li@fruits{ span[data-color=$color]{$name}};`)
    more && console.log(`    li@ids{&[data-fruit-id=$id];} -> {fruits: [{ color: 'red', name: apple}], ids: [{id: '1' }]}`)
    more && console.log(`赋值的例子`)
    more && console.log(``)
    more && console.log(`    $top = 'level';`)
    more && console.log(`    ul { $hasUlElement = true };`)
    more && console.log(`    div { $hasDivElement = true };`)
    more && console.log(`    li@array {`)
    more && console.log(`        $row = true;`)
    more && console.log(`        $isRed = false;`)
    more && console.log(`        [data-color=red]{ $isRed = true };`)
    more && console.log(`    }; -> { top: 'level', hasUlElement: true, array: [ { row: true, isRed: true } ] }`)
    more && console.log(`    过滤器`)
    more && console.log(`    $foo|xxx 放在值捕获的右边；xxx 是过滤器的名字`)
    more && console.log(`    @bar|xxx 放在数组捕获的右边；`)
    more && console.log(`    $foo|xxx(arg1, arg2, ...) 过滤器可以接受若干个参数；每一个参数都是一个 JavaScript 字面量；`)
    more && console.log(`    $foo|f1(a,b)|f2 过滤器可以进行串联。`)
    more && console.log(`    Structure Manipulation Filters: 该部分包括 pack，flatten，compact，first，last，get。`)
    more && console.log(`    Type Coercion Filters: 该部分包括 String，Number，Date，Boolean。这些过滤器用于将输入转换为指定的类型。`)
    more && console.log(`    Prototype Filters: 我们可以使用来自原型链的方法。举个例子，如果我们可以保证 x 每次被捕获的时候其类型总是字符串, 那么我们可以安全地使用 $x|substring(0, 20) 或是 $x|toUpperCase 或是 $x|trim。`)
    more && console.log(`temme('<div>1 22 333 4444</div>', div{ $|split(' ')||Number }) -> [1, 22, 333, 4444]`)
    more && console.log(``)
    more && console.log(`   tr@{`)
    more && console.log(`        &[class=$class];`)
    more && console.log(`        >td@list{&{$text}}`)
    more && console.log(`    }`)
    more && console.log(`    -> [{class:'...', list: [{text: '...'}, ...]}, ...]`)
    more && console.log(``)
    more && console.log(`    tr@{`)
    more && console.log(`        >td@{`)
    more && console.log(`            &[bgcolor="#c4dcf3"]{ $label = true };`)
    more && console.log(`            &[class="color"]{ $label = true };`)
    more && console.log(`            &[colspan=$colspan][rowspan=$rowspan]{html($text)};`)
    more && console.log(`        }`)
    more && console.log(`    }`)
    more && console.log(`    -> { label: true, colspan: '2', text: '姓&nbsp;&nbsp;名' }`)
  } else if (item === 'dhtml') {
    console.log(`获取动态网页html: `)
    console.log(`->dhtml({ encoding = 'utf-8', inFormat, outFormat, executablePath })`)
    console.log(`    encoding: 默认是 utf-8， 可以是 gb2312|utf-8|GBK，解决网页汉字乱码的问题，只适用于流入参存在html`)
    console.log(`    inFormat: 修正流入参，遵循 parseFunction`)
    console.log(`    outFormat: 修正流出参(html)，遵循 parseFunction`)
    console.log(`    executablePath: puppeteer使用的浏览器exe路径，默认为: C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe`)
    console.log(`流入参数: `)
    console.log(`    流入参格式: { url, selector, prepares, hasCookie }`)
    console.log(`    流入参的selector字段是类如 div, .clasName, #id 等选择器，用于网页判断该组件是否被加载`)
    console.log(`    prepares 执行输入和点击，如登陆等问题: 如: input[name='username']:admin;input[name='password']:123456;button:click`)
    console.log(`    hasCookie 是否使用 Cookie`)
    console.log(`实例：`)
    console.log(`    xkettle -s "@list([{ url: 'https://www.meipian.cn/2lsnwc89', selector: '.mp-article-item-area'}])->dhtml({ outFormat: o=>({ html: o, selector: '.mp-article-item-area' }) })->log()"`)
  } else if (item === 'article') {
    console.log(`图文爬虫: `)
    console.log(`->article({ encoding='utf-8', inFormat, outFormat, srcName, imagePath, seprateLevel=1 })`)
    console.log(`    encoding: 默认是 utf-8， 可以是 gb2312|utf-8|GBK，解决网页汉字乱码的问题，只适用于流入参存在html`)
    console.log(`    inFormat: 修正流入参，遵循 parseFunction`)
    console.log(`    outFormat: 修正流出参(list)，遵循 parseFunction`)
    console.log(`    srcName: 图片的src名称，默认为src，如果为多个，用逗号隔开，优先级高的在前面，如: data-src,src`)
    console.log(`    imagePath: 图片保存的地址，如果为空，则不保存图片，使用网络地址`)
    console.log(`    seprateLevel:  默认是1，分段落的极数，如果等于这个等级后，子级所有的text合并为一行`)
    console.log(`流入参数: `)
    console.log(`    流入参格式: { html, selector } 或者 { url, selector }`)
    console.log(`    流入参的selector字段是类如 div, .clasName, #id 等选择器`)
    console.log(`    以selector为根节点，搜索所有子节点的图文，格式为: [{ text: 'xx'}, { url: 'xx'}]`)
    console.log(`    如果没有 selector, 则以现有的根节点为根节点`)
  } else if (item === 'imageSave') {
    console.log(`保存图片: `)
    console.log(`->imageSave({ urlName = 'url', path = '.', inFormat, outFormat })`)
    console.log(`    urlName: 需要下载的图片的url的字段名称，默认为url，执行后会被替换为新的url`)
    console.log(`    path: 图片下载的路径，新的url为: path + uuid + extname`)
    console.log(`    inFormat: 修正流入参，遵循 parseFunction`)
    console.log(`    outFormat: 修正流出参(list)，遵循 parseFunction`)
  } else if (item === 'console') {
    console.log(`输出到控制台，流不变`)
    console.log(`->console(type, copy)`)
    console.log(`    type:  0: js 1: json 2:inline-js 3:inline-json`)
    console.log(`    如果type是函数，则使用函数修正list，遵循 parseFunction`)
    console.log(`    如果type是字符串， 逐行打印inline-js格式，每行后面追加该字符串`)
    console.log(`    copy: 拷贝到剪切板`)
    console.log(`例子：`)
    console.log(`    ->console() // 逐行打印如果是字符串，则打印原值，否则用inline-js格式`)
    console.log(`    ->console(',') // 逐行打印如果是字符串，则打印原值，否则用inline-js格式，每行后面追加,号`)
    console.log(`    ->console('!,') // 逐行打印强制用inline-js格式，每行后面追加,号`)
    console.log(`    ->console(0) // 逐行打印js格式`)
    console.log(`    ->console(2) // 逐行打印inline-js格式`)
    console.log(`    ->console(10) // 打印所有list为js格式`)
    console.log(`    ->console(12) // 打印所有list为inline-js格式`)
    console.log(`    ->console(list=>JSON.stringify(list)) // 打印所有list为JSON字符串`)
  } else if (item === 'excel') {
    console.log(`写入到excel文件，流不变`)
    console.log(`->excel(file, sheetName = 'Sheet1')`)
    console.log(`    如果object是对象，则第一版就是对象的keys`)
  } else if (item === 'md') {
    console.log(`写入到markdown文件，流不变`)
    console.log(`->md(file)`)
  } else if (item === 'text') {
    console.log(`->text(file, widths = {})`)
    console.log(`    widths指定字段宽度，可以使用key和index指定，默认为20`)
  } else if (item === 'file') {
    console.log(`写入到文件，流不变`)
    console.log(`->file(file, { inFormat, begin, end })`)
    console.log(`    如果解析file为字符串，则所有的流都写入到该名称文件中`)
    console.log(`    如果解析file为函数，则文件名为: file(chunk, { _, moment, utils })，每个流分别写入不同的文件`)
    console.log(`    inFormat格式化thunk，遵循 parseFunction`)
    console.log(`    begin为开头，end为尾部，每行都有都有头和尾，写入格式如下: `)
    console.log(`格式：`)
    console.log(`    [begin]`)
    console.log(`        \${o => inFormat(o)} ...`)
    console.log(`    [end]`)
  } else if (item === 'json') {
    console.log(`写入到json文件，流不变`)
    console.log(`->json(file, { inFormat })`)
    console.log(`    inFormat格式化thunk，遵循 parseFunction`)
    console.log(`    写入JSON.stringify(obj)数据`)
  } else if (item === 'js') {
    console.log(`写入到js文件，流不变，会自动添加module.exports导出结果`)
    console.log(`->js(file, { type = 0, inFormat })`)
    console.log(`    type: 0: js 1: json 2:inline-js 3:inline-json`)
    console.log(`    inFormat格式化thunk，遵循 parseFunction`)
  } else if (item === 'mongodb') {
    console.log(`写入到mongodb，流不变`)
    console.log(`->mongodb({ uri = 'mongodb://localhost:27017', database, table })`)
    console.log(`    将数据写入道mongodb的数据库database的table中`)
  } else if (item === 'mysql') {
    console.log(`写入到mysql，流不变`)
    console.log(`->mysql({ host = 'localhost', port = 3306, user = 'root', password, database, table, fields })`)
    console.log(`    将数据写入道mysql的数据库database的table中`)
    console.log(`    如果没有表，需要传入fields创建表，如: { phone: 'VARCHAR(11)', password: 'VARCHAR(50)' }`)
  } else if (item === 'oracle') {
    console.log(`写入到oracle，流不变`)
    console.log(`->oracle({ user, password, connectString, table, fields })`)
    console.log(`    将数据写入道oracle的数据库database的table中`)
    console.log(`    如果没有表，需要传入fields创建表，如: { phone: 'VARCHAR(11)', password: 'VARCHAR(50)' }`)
  } else if (item === 'sqlserver') {
    console.log(`写入到sqlserver，流不变`)
    console.log(`->sqlserver({ user, password, server, database, table, fields })`)
    console.log(`    将数据写入道sqlserver的数据库database的table中`)
    console.log(`    如果没有表，需要传入fields创建表，如: { phone: 'VARCHAR(11)', password: 'VARCHAR(50)' }`)
  }
}
async function createStream(name, params, env, isRead) {
  debug && console.log(`发现流:${name}(${params})`);
  try {
    params = params ? `[${params}]` : '[]';
    params = (new Function(`return ($, _, moment, utils)=>${params}`)())(env, _, moment, utils);
  } catch (e) {
    console.log('createStream [throw]:', e);
    throw new Error(`参数解析错误：${e.error}，方法：${name}，参数：${params}`);
  }
  let stream;
  if (isRead) {
    stream = kettle[`${name}Reader`];
    if (!stream) {
      throw new Error(`不存在 ${name} 读出流`);
    }
    return await stream(...params);
  }
  stream = kettle[`${name}Writer`];
  if (!stream) {
    stream = kettle[`${name}Transform`];
    if (!stream) {
      throw new Error(`不存在 ${name} 转换流`);
    }
  }
  return await stream(...params);
}

function startServer(servers, streams) {
  const item = streams[0];
  debug && console.log(`创建服务器接口:${item.name}(${item.params})`);
  const r = utils.eval(item.params ? `[${item.params}]` : '[]');
  if (r.error) {
    throw new Error(`参数解析错误：${r.error}，方法：${item.name}，参数：${item.params}`);
  }
  const params = r.value;
  const api = params[0];
  if (!api) {
    throw new Error(`服务器没有设置api`);
  }
  let paramSet, option = { p: 5189, t: 0 };
  if (!_.isFunction(params[1])) {
    option = { ...option, ...(params[1] || {}) };
    paramSet = o => o;
  } else {
    paramSet = params[1];
    option = { ...option, ...(params[2] || {}) };
  }
  const port = option.p;
  let server = servers[port];
  if (!server) {
    server = { port };
    const express = require('express');
    const bodyParser = require('body-parser');
    server.app = express();
    server.app.all('*', (req, res, next) => {
      res.header('Access-Control-Allow-Origin', '*');
      res.header('Access-Control-Allow-Headers', 'Content-Type');
      next();
    });
    server.app.use(bodyParser.urlencoded({ extended: false }));
    server.app.use(bodyParser.json());
    server.app.use(bodyParser.text());
    server.app.listen(option.p, () => {
      console.log(`服务器开始监听: http://localhost:${option.p}`);
    });
    server.streams = {};
    servers[option.p] = server;
  }
  server.streams[api] = streams.slice(1);
  if (item.name === 'get') {
    console.log(`注册get接口: http://localhost:${option.p}${api}`);
    server.app.get(api, async (req, res) => {
      option.t && await utils.sleep(option.t * 1000);
      startStream(server.streams[api], paramSet(req.query, utils), res);
    });
  } else {
    console.log(`注册post接口: http://localhost:${option.p}${api}`);
    server.app.post(api, async (req, res) => {
      option.t && await utils.sleep(option.t * 1000);
      startStream(server.streams[api], paramSet(req.body, utils), res);
    });
  }
}
async function startStream(streams, env, res) {
  let hasSend = false; // 是否发送
  const namedStreams = {};
  // 执行流
  let pre;
  for (const item of streams) {
    let stream;
    if (item.isTemp) {
      stream = namedStreams[item.name];
    } else {
      stream = await createStream(item.name, item.params, env, item.isRead);
      if (item.temp) {
        namedStreams[item.temp] = stream;
      }
    }
    if (!pre) {
      pre = stream;
    } else {
      pre = pre.pipe(stream);
    }
  }
  if (!hasSend && res) {
    hasSend = true;
    pre = pre.pipe(kettle.responseWriter(res));
  }
  const writer = new Writable({ objectMode: true });
  writer._write = (chunk, encoding, callback) => { callback() };
  writer.on('finish', () => { console.log('执行完毕'); });
  pre.pipe(writer);
}
async function parseStream(line) {
  debug && console.log('[parseStream]line =', line);
  line = line.trim();
  let i = -1, max = line.length - 1, item = '';
  let escape = 0, ins = 0, inS = 0, isRead = 0, isTrans = 0, id = 1;
  let groups = [], namedStreams = {}, current = [], server;
  while (i++ < max) {
    const ch = line[i];
    if (ch === '\\') {
      escape = 1;
      item = `${item}${ch}`;
      continue;
    }
    if (!escape && !ins && !inS) {
      if (ch === '@') {
        isRead = true;
        item = '';
        continue;
      }
      if (ch === '-' && line[i + 1] === '>') {
        item = item.trim();
        if (isRead && item) { // 分流开始
          if (!namedStreams[item]) {
            throw new Error(`没有名称为 ${item} 的流`);
          }
          isRead = false;
          current.push(namedStreams[item]); // 保存命名流
        }
        isTrans = true;
        i++;
        item = '';
        continue;
      }
      if ((isRead || isTrans) && ch === '(') {
        const block = utils.getBlockLine(line.slice(i), '(', ')');
        debug && console.log('[parseStream]发现参数开始', { block, item, line: line.slice(i), i, isRead });
        i += block.length - 1;
        item = item.replace(/\s*/g, '');
        const match = item.match(/^\[(.+)](.+)$/);
        let temp; // 命名流
        if (match) {
          temp = match[1]; // 发现命名流
          item = match[2];
        }
        if (isRead) {
          if (current.length && !current[0].isServer) {
            groups.push(current); // 保存当前流
            current = [];
          } else if (item === 'get' || item === 'post') { // 服务开始
            current.length && groups.push(current); // 保存当前流
            current = [{
              id: id++,
              name: item,
              params: block.slice(1, -1),
              isServer: true,
            }];
            continue;
          }
        }
        current.push({
          id: id++,
          name: item,
          params: block.slice(1, -1),
          temp,
          isRead,
        });
        temp && (namedStreams[temp] = { name: temp, isTemp: true }); // 临时流
        item = '';
        isRead = false;
        isTrans = false;
        debug && console.log('[parseStream]发现参数结束', { block, item, line: line.slice(i), i });
        continue;
      }
    }
    if (ch === `'` && !escape && !inS) {
      ins = !ins;
    } else if (ch === `"` && !escape && !ins) {
      inS = !inS;
    }
    item = `${item}${ch}`;
    escape = 0;
  }
  groups.push(current); // 保存当前流
  debug && console.log('流：', groups);
  const servers = {};
  for (const streams of groups) {
    if (streams[0] && streams[0].isServer) {
      startServer(servers, streams);
    } else {
      startStream(streams);
    }
  }
}
function command(line) {
  parseStream(utils.deleteComment(line));
}
function main() {
  const params = process.argv.slice(2);
  let index = params.indexOf('-h'); // 检查帮助模式
  (index == -1) && (index = params.indexOf('--help'));
  if (index > -1) {
    return showHelp(params[index + 1], params[index + 2]);
  }

  index = params.indexOf('-v'); // 检查日志模式
  if (index > -1) {
    utils.DEBUG = true;
    params.splice(index, 1);
  }
  index = params.indexOf('-s'); // 检查脚本模式
  if (index > -1) {
    if (!params[index + 1]) {
      return console.log(`参数错误`);
    }
    global.$params = params.slice(2); // 全局参数
    return command(params[index + 1]);
  }
  if (!params[0]) {
    return console.log(`参数错误`);
  }
  if (!fs.existsSync(params[0])) {
    return console.log(`文件不存在`);
  }
  global.$params = params.slice(1); // 全局参数
  command(fs.readFileSync(params[0], 'utf-8'));
}

main()
